Never advance the chain past a record that was not written - #41
Merged
Conversation
emit() advanced chainIndex and previousHash before the sinks ran and swallowed whatever they threw. A record that never reached disk still moved the chain on, so the next record carried an index jump and a broken link: the shape of a DELETED record. `agentwall verify` reported a full partition with the same findings as the corpus forgery b3-record-removed, while the process stayed up and said nothing. The chain now advances only after a durable sink accepts the record, so the file stays contiguous across a loss and neither the index nor the link reads as an edit. The refused payload goes to stderr under `agentwall_audit_dropped` with no integrity block, /health carries the counters, and the first append that succeeds afterwards writes a gap declaration record that both verifiers report as the non-fatal chain-gap-declared. A declaration never excuses an index gap, a link break, or a hash mismatch. Two failures found alongside it. A short write left a fragment with no terminator that the next append fused onto, so a full disk destroyed a record that WAS written on top of the one that was not; the sink now rolls its own partial write back. And a stream redirected to a regular file reports a failed writeSync as an 'error' event rather than an exception, so the per-sink try/catch never saw it and an unhandled event killed the process on the next tick, taking egress gating down with it. Also: remove capability tickets, which /evaluate minted and nothing ever presented back, and cover the decision precedence conflict that no test reached.
repsecure
force-pushed
the
agent/3.0-durable-append
branch
from
August 5, 2026 22:31
185b11e to
8729f26
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Never advance the chain past a record that was not written.
The defect
Chain state advanced before the sinks ran, so when
appendFileSynchit ENOSPC the record never reached disk while the in-memory chain had already moved on. Verification then reported an index gap and a broken link, which is byte-identical to corpus case b3, an attacker deleting a record. A full disk and a deletion were indistinguishable, and nothing was logged.Reproduced on a 128k tmpfs filled to zero available blocks, confirming appends genuinely stopped landing rather than assuming it.
The fix
The chain advances only after a durable sink accepts the record, so a lost record leaves no index jump and no broken link. Contiguity alone would make the loss invisible, so the loss is then declared: the refused payload goes to stderr,
/healthcarries counters, and the first successful append writes an in-chain gap declaration that both verifiers report as the non-fatalchain-gap-declared.Sinks are now typed durable or observational. Observers run only after the evidence stream accepts, so a console can never display a record the chain does not contain, and an observer that throws cannot hold the chain back.
Halting the process was rejected:
src/index.tsis explicit that neither the chain nor the dashboard may break egress, and halting does not empty a disk. It converts a logging outage into a gating outage.Two further defects found while fixing it
Short writes fused records. ENOSPC can write part of a line and then fail, leaving a headless fragment with no terminator, and the next append landed on that same line. A full disk therefore destroyed a record that was written on top of the one that was not. The sink now rolls a partial write back to a mark taken at the last completed append, guarded so a truncate can never eat whole records.
A failed console write killed the process. Pre-existing on main.
Also closed
verifyCapabilityTicketSignaturewas exported, tested, and called from nowhere, and policy precedence had no conflict test. Both addressed.